use crate::tables::user::TbUser;
use crate::utils::crypto;
use crate::{handlers::Payload, tables::log::TbLog};
use actix_web::{post, web, HttpRequest, HttpResponse};
use rbdc::datetime::DateTime;
use serde::{Deserialize, Serialize};
use serde_json::json;

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Login {
    username: String,
    password: String,
    auto_login: Option<bool>,
    r#type: String,
    time: i64,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct LoginResult {
    status: String,
    r#type: String,
    current_authority: Option<String>,
    token: Option<String>,
}

impl LoginResult {
    fn new(status: String, r#type: String) -> Self {
        Self {
            status,
            r#type,
            current_authority: None,
            token: None,
        }
    }

    fn set_current_authority(mut self, current_authority: String) -> Self {
        self.current_authority = Some(current_authority);
        self
    }

    fn set_token(mut self, token: String) -> Self {
        self.token = Some(token);
        self
    }
}

#[post("/login")]
async fn login(data: web::Json<Login>, req: HttpRequest) -> HttpResponse {
    let now = chrono::Utc::now().timestamp();
    if now < data.time || (now - data.time) > 24 * 3600 {
        return HttpResponse::Ok().json(
            LoginResult::new("error".into(), data.r#type.to_owned())
                .set_current_authority("guset".into()),
        );
    }

    let user = TbUser::get_user(data.username.as_str()).await;
    if user.is_err() {
        return HttpResponse::Ok().json(
            LoginResult::new("error".into(), data.r#type.to_owned())
                .set_current_authority("guset".into()),
        );
    }
    let user = user.unwrap();
    if user.is_none() {
        return HttpResponse::Ok().json(
            LoginResult::new("error".into(), data.r#type.to_owned())
                .set_current_authority("guset".into()),
        );
    }

    let user = &mut user.unwrap();

    if crypto::md5(&(data.time.to_string() + user.password.as_str())) != data.password {
        return HttpResponse::Ok().json(
            LoginResult::new("error".into(), data.r#type.to_owned())
                .set_current_authority("guset".into()),
        );
    }

    user.last_login_ts = Some(DateTime::from_timestamp(now));
    let result = TbUser::update_by_id(user).await;
    if result.is_err() {
        return HttpResponse::Ok().json(
            LoginResult::new("error".into(), data.r#type.to_owned())
                .set_current_authority("guset".into()),
        );
    }

    let result = TbLog::insert_log(
        &TbLog::new(data.username.to_owned(), "登录").set_data(json!({
            "time": chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(),
            "ip": req.connection_info().realip_remote_addr(),
        })),
    )
        .await;
    if result.is_err() {
        return HttpResponse::Ok().json(
            LoginResult::new("error".into(), data.r#type.to_owned())
                .set_current_authority("guset".into()),
        );
    }

    let payload = serde_json::to_string(&Payload {
        username: data.username.clone(),
        role_id: user.role_id,
        time: now,
    });
    if payload.is_err() {
        return HttpResponse::Ok().json(
            LoginResult::new("error".into(), data.r#type.to_owned())
                .set_current_authority("guset".into()),
        );
    }

    HttpResponse::Ok().json(match crypto::generate_jwt(payload.unwrap().as_str()) {
        Ok(token) => LoginResult::new("ok".into(), data.r#type.to_owned())
            .set_current_authority("admin".into())
            .set_token(token),
        Err(_) => LoginResult::new("error".into(), data.r#type.to_owned())
            .set_current_authority("guset".into()),
    })
}

#[post("/logout")]
async fn logout() -> HttpResponse {
    HttpResponse::Ok().json(json!({
        "success": true
    }))
}

pub fn routes(cfg: &mut web::ServiceConfig) {
    cfg.service(login).service(logout);
}

#[cfg(test)]
pub mod tests {
    use actix_web::{test, App};

    use crate::{
        handlers::{self, account},
        utils::mysql,
    };

    #[actix_web::test]
    async fn test_login() {
        mysql::init_pool().await;

        let token = handlers::tests::get_token().await;
        println!("{}", token);
    }

    #[actix_web::test]
    async fn test_logout() {
        mysql::init_pool().await;

        let app = test::init_service(App::new().configure(account::routes)).await;

        let req = test::TestRequest::post().uri("/logout").to_request();

        let resp = test::call_service(&app, req).await;
        let status = resp.status();
        println!("resp status is {:?}", status);
        let resp_body = test::read_body(resp).await;
        println!("restp body is {:?}", resp_body);
        assert!(status.is_success());
    }
}
